// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/ftx_futures/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <rapidjson/document.h>

#include "coin/proto/coin_query.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/ftx_common/api_order/fix/fix_fields.h"
#include "coin2/exchange/util/json_util.h"
#include "quickfix/Message.h"

namespace coin2::exchange::ftx_futures::order_v1 {

namespace {

using namespace std::literals;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductFillElement;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::ProductPosition;
using coin::proto::SubmitOrderResponse;
using coin::proto::CurrencyTransferElement;
using coin::proto::CurrencyTransferBundle;
using coin::proto::TransferRequestProto;

using IOrderContext = base::order::IOrderContext;
using symbology::FtxCurrency;
using symbology::FtxFuturesProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using OrderEventType = coin::proto::OrderEvent::OrderEventType;
using FtxOrderSubTopicId = coin2::exchange::ftx_common::api_order::FtxOrderSubTopicId;

inline int64_t GetProcOrderId(const JsonValue& data) {
  int64_t proc_order_id = 0;
  if (data.HasMember("clientId") && data["clientId"].IsString()) {
    // handle clientId is not numberic, eg. place order from web UI
    try {
      proc_order_id = stoll(data["clientId"].GetString());
    } catch (const std::exception&) {
    }
  }
  return proc_order_id;
}

inline int64_t GetOrderTimestamp(const JsonValue& data) {
  std::string time_str = data["time"].GetString();
  time_str.replace(time_str.begin() + 23, time_str.end(), "Z");
  return TimestampFromIso8601(time_str);
}

inline coin::proto::OrderSide GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

inline void SetOrderStatus(const JsonValue& data, ProductOrderElement* order) {
  std::string status = data["status"].GetString();
  if (status == "new" || status == "open") {
    order->set_is_live(true);
  } else if (status == "closed") {
    if (std::string(data["filledSize"].GetString()) == std::string(data["size"].GetString())) {
      order->set_fully_filled(true);
    } else {
      order->set_cancel_confirmed(true);
    }
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << status;
  }
}

inline bool IsFtxErrMessage(const JsonValue& doc) { return doc.HasMember("error"); }

inline std::string GetFtxExchangeOrderErrorMsg(const JsonValue& json) {
  return json["error"].GetString();
}

inline coin::proto::OrderErrorCode GetOrderErrorCodeFromFtx(const JsonValue& json_error_msg) {
  using coin::proto::OrderErrorCode;
  auto result = coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR;
  std::string error_code(json_error_msg.GetString());

  if (error_code == "0") {
    result = OrderErrorCode::ORDER_NO_ERROR;
  } else if (error_code == "Not enough balances") {
    result = OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND;
  } else if (error_code == "Order already closed") {
    result = OrderErrorCode::ORDER_NOT_FOUND;
  } else if (error_code == "Order not found") {
    result = OrderErrorCode::ORDER_NOT_FOUND;
  } else {
    LOG(ERROR) << "Unknown error code: " << error_code;
  }

  return result;
}

bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  return json.HasMember("type") && (std::string(json["type"].GetString()) == "update");
}

}  // namespace

void FtxFuturesParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

bool FtxFuturesParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  {
    "result": [{
      "coin": "USD",
      "free": -7.32967559,
      "total": -7.32967559,
      "usdValue": -7.32967559
    }],
    "success": true
  }
  */
  if (doc.IsObject() && IsFtxErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }
  if (!(doc.HasMember("result") && doc["result"].IsArray())) {
    LOG(ERROR) << "[ParseBalance] unexpected balance data! " << JsonToString(doc);
    return false;
  }

  for (const auto& data : doc["result"].GetArray()) {
    if (!data.HasMember("coin") || !data.HasMember("free") || !data.HasMember("total")) {
      THROW() << "Parse error in account balance" << JsonToString(data);
    }
    CurrencyBalance balance;
    balance.set_currency_native(data["coin"].GetString());
    double current_free = stod(data["free"].GetString());
    double current_total = stod(data["total"].GetString());
    balance.set_available(current_free);
    balance.set_total(current_total);
    balance.set_hold(current_total - current_free);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance FtxFuturesParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Ftx");
  acc_balance.set_market_type("Futures");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }
  is_account_balance_initialized_ = true;

  return acc_balance;
}

void FtxFuturesParser::ParseRestAccountPosition() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestAccountPosition] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_position = ParsePosition(doc);
  if (!account_position.has_exchange() || !account_position.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  result_.set_type(MsgType::ACCOUNT_POSITION);
  *result_.mutable_account_position() = std::move(account_position);
}

bool FtxFuturesParser::ParseRawPosition(const JsonValue& doc, RawPositionMap* raw_position_map) {
  /*
  {
    "success": true,
    "result": [
      {
        "cost": -31.7906,
        "entryPrice": 138.22,
        "estimatedLiquidationPrice": 152.1,
        "future": "ETH-PERP",
        "initialMarginRequirement": 0.1,
        "longOrderSize": 1744.55,
        "maintenanceMarginRequirement": 0.04,
        "netSize": -0.23,
        "openSize": 1744.32,
        "realizedPnl": 3.39441714,
        "shortOrderSize": 1732.09,
        "side": "sell",
        "size": 0.23,
        "unrealizedPnl": 0
      }
    ]
  }
  */
  if (doc.IsObject() && IsFtxErrMessage(doc)) {
    LOG(ERROR) << "[ParsePosition] " << JsonToString(doc);
    return false;
  }
  if (!(doc.HasMember("result") && doc["result"].IsArray())) {
    LOG(ERROR) << "[ParsePosition] unexpected position format! " << JsonToString(doc);
    return false;
  }

  for (const auto& data : doc["result"].GetArray()) {
    const auto& native_symbol = data["future"].GetString();
    if (raw_position_map->count(native_symbol) == 0) {
      raw_position_map->emplace(native_symbol, ProductPosition{});
    }
    auto& position = raw_position_map->at(native_symbol);
    double current_qty = stod(data["netSize"].GetString());
    if (current_qty > 0) {
      position.set_long_position(current_qty);
    } else if (current_qty < 0) {
      position.set_short_position(-current_qty);
    }
    position.set_net_position(current_qty);

    position.set_realized_pnl(stod(data["realizedPnl"].GetString()));
    position.set_unrealized_pnl(stod(data["unrealizedPnl"].GetString()));
    position.set_total_pnl(position.realized_pnl() + position.unrealized_pnl());
  }

  return true;
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountPosition FtxFuturesParser::ParsePosition(const JsonValue& doc) {
  RawPositionMap raw_position_map;
  auto success = ParseRawPosition(doc, &raw_position_map);
  if (!success) {
    return {};
  }

  AccountPosition acc_position;
  acc_position.set_exchange("Ftx");
  acc_position.set_market_type("Futures");
  for (const auto& native_symbol : symcache_->GetProductNative()) {
    auto* position = acc_position.add_each_position();
    if (raw_position_map.count(native_symbol) == 0) {
      position->set_long_position(0);
      position->set_short_position(0);
      position->set_net_position(0);
      position->set_available_long_position(0);
      position->set_available_short_position(0);
    } else {
      position->MergeFrom(raw_position_map.at(native_symbol));
    }
    auto* product = symcache_->GetProductByNative(native_symbol);
    position->set_symbol(product->symbol());
    position->set_native_symbol(product->order_native_symbol());
    DLOG(INFO) << "POS: " << product->symbol() << " " << position->net_position();
  }
  is_account_position_initialized_ = true;

  return acc_position;
}

ProductOrderElement FtxFuturesParser::ParseOrderInfo(const JsonValue& data) {
  ProductOrderElement order;
  order.set_market_type(MarketType::Futures);
  order.set_exchange_type(ExchangeType::Ftx);
  order.set_api_version("v1");
  order.set_exchange_order_id(data["id"].GetString());
  order.set_symbol(data["market"].GetString());
  if (data["price"].IsString()) {
    auto price = data["price"].GetString();
    if (price != "null"sv) {
      order.set_price(stod(price));
    }
  }
  order.set_qty(stod(data["size"].GetString()));
  order.set_proc_order_id(GetProcOrderId(data));
  order.set_side(GetOrderSide(data["side"].GetString()));
  order.set_order_type(GetOrderType(data["type"].GetString()));
  order.set_timestamp(msg_->timestamp);
  SetOrderStatus(data, &order);
  return order;
}

void FtxFuturesParser::ParseRestOrdersInfo() {
  /*
  {
    "success": true,
    "result": [
      {
        "avgFillPrice": 10135.25,
        "clientId": null,
        "createdAt": "2019-06-27T15:24:03.101197+00:00",
        "filledSize": 0.001,
        "future": "BTC-PERP",
        "id": 257132591,
        "ioc": false,
        "market": "BTC-PERP",
        "postOnly": false,
        "price": 10135.25,
        "reduceOnly": false,
        "remainingSize": 0.0,
        "side": "buy",
        "size": 0.001,
        "status": "closed",
        "type": "limit"
      },
    ],
    "hasMoreData": false,
  }
  */
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestOrdersInfo] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsFtxErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(data);
    return;
  }

  if (!(data.HasMember("result") && data["result"].IsArray())) {
    LOG(ERROR) << "Unexpected order list format!" << JsonToString(data);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (auto& info : data["result"].GetArray()) {
    auto order = product_order_bundle->add_each_order();
    auto new_order = ParseOrderInfo(info);
    order->MergeFrom(new_order);
    product_order_bundle_found = true;
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

ProductOrderBundle FtxFuturesParser::ParseWsOrderInfoListV2(const JsonValue& data) {
  /*
  {
    "channel": "orders",
    "data": {
      "id": 24852229,
      "clientId": null,
      "market": "XRP-PERP",
      "type": "limit",
      "side": "buy",
      "size": 42353.0,
      "price": 0.2977,
      "reduceOnly": false,
      "ioc": false,
      "postOnly": false,
      "status": "closed",
      "filledSize": 0.0,
      "remainingSize": 0.0,
      "avgFillPrice": 0.2978
    },
    "type": "update"
  }
  */
  ProductOrderBundle product_order_bundle;

  auto order = product_order_bundle.add_each_order();
  auto new_order = ParseOrderInfo(data["data"]);
  order->MergeFrom(new_order);

  return product_order_bundle;
}

ProductFillElement FtxFuturesParser::ParseFillInfo(const JsonValue& data) {
  ProductFillElement fill;
  // linchuan: ignore the fills long ago
  int64_t fill_timestamp_et = GetOrderTimestamp(data);
  int64_t fill_timestamp_ft = msg_->timestamp;
  if (fill_timestamp_ft - fill_timestamp_et > 100 * 1e9) {
    return fill;
  }
  fill.set_symbol(data["market"].GetString());
  fill.set_fill_timestamp(fill_timestamp_ft);
  fill.set_exchange_order_id(data["orderId"].GetString());
  fill.set_proc_order_id(GetProcOrderId(data));
  fill.set_price(stod(data["price"].GetString()));
  fill.set_qty(stod(data["size"].GetString()));
  fill.set_fill_id(data["id"].GetString());
  std::string fill_type = data["liquidity"].GetString();
  if (fill_type == "maker") {
    fill.set_fill_type(FillType::MAKER_FILL_TYPE);
  } else if (fill_type == "taker") {
    fill.set_fill_type(FillType::TAKER_FILL_TYPE);
  } else {
    fill.set_fill_type(FillType::UNKNOWN_FILL_TYPE);
  }
  if (data.HasMember("fee") && !data["fee"].IsNull()) {
    if (std::string fee = data["fee"].GetString(); !fee.empty()) {
      fill.set_fee(stod(fee));
    }
  }
  if (data.HasMember("feeCurrency") && !data["feeCurrency"].IsNull()) {
    if (std::string fee_currency = data["feeCurrency"].GetString(); !fee_currency.empty()) {
      fill.set_fee_currency(fee_currency);
    }
  }

  return fill;
}

void FtxFuturesParser::ParseRestFillInfoList() {
  /*
  {
    "success": true,
    "result": [
      {
        "fee": 20.1374935,
        "feeCurrency": "USD",
        "feeRate": 0.0005,
        "future": "EOS-0329",
        "id": 11215,
        "liquidity": "taker",
        "market": "EOS-0329",
        "baseCurrency": null,
        "quoteCurrency": null,
        "orderId": 8436981,
        "tradeId": 1013912,
        "price": 4.201,
        "side": "buy",
        "size": 9587,
        "time": "2019-03-27T19:15:10.204619+00:00",
        "type": "order"
      }
    ]
  }
  */
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsFtxErrMessage(data)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data);
    return;
  }

  if (!(data.HasMember("result") && data["result"].IsArray())) {
    LOG(ERROR) << "[ParseRestFillInfoList] Unexpected fill list format!" << JsonToString(data);
    return;
  }
  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (auto& info : data["result"].GetArray()) {
    auto new_fill = ParseFillInfo(info);
    if (new_fill.has_fill_id()) {
      auto fill = product_fill_bundle->add_each_fill();
      fill->MergeFrom(new_fill);
      product_fill_bundle_found = true;
    }
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

ProductFillBundle FtxFuturesParser::ParseWsFillInfoListV2(const JsonValue& data) {
  /*
  {
    "channel": "fills",
    "data": {
      "fee": 78.05799225,
      "feeRate": 0.0014,
      "future": "BTC-PERP",
      "id": 7828307,
      "liquidity": "taker",
      "market": "BTC-PERP",
      "orderId": 38065410,
      "tradeId": 19129310,
      "price": 3723.75,
      "side": "buy",
      "size": 14.973,
      "time": "2019-05-07T16:40:58.358438+00:00",
      "type": "order"
    },
    "type": "update"
  }
  */
  ProductFillBundle product_fill_bundle;
  auto fill = product_fill_bundle.add_each_fill();
  auto new_fill = ParseFillInfo(data["data"]);
  fill->MergeFrom(new_fill);

  return product_fill_bundle;
}

void FtxFuturesParser::ParseSubmitResponse() {
  /*
  {
    "success": true,
    "result": {
      "createdAt": "2019-03-05T09:56:55.728933+00:00",
      "filledSize": 0,
      "future": "XRP-PERP",
      "id": 9596912,
      "market": "XRP-PERP",
      "price": 0.306525,
      "remainingSize": 31431,
      "side": "sell",
      "size": 31431,
      "status": "open",
      "type": "limit",
      "reduceOnly": false,
      "ioc": false,
      "postOnly": false,
      "clientId": null,
    }
  }
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[FtxSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[FtxSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[FtxSubmitOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (!json.IsObject()) {
    LOG(ERROR) << "[FtxSubmitOrder] Not object " << http_context->res;
    resp->set_success(false);
    return;
  }

  if (json.HasMember("error")) {
    auto error_code = GetOrderErrorCodeFromFtx(json["error"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetFtxExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[FtxSubmitOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  if (!(json.HasMember("result") && json["result"].HasMember("id"))) {
    LOG(ERROR) << "[FtxSubmitOrder] exchange order id not found\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }

  resp->set_exchange_order_id(json["result"]["id"].GetString());
  resp->set_success(true);
}

void FtxFuturesParser::ParseCancelResponse() {
  /*
  {
    "success": true,
    "result": "Order queued for cancelation"
  }
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[FtxCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[FtxCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[FtxCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (json.HasMember("error")) {
    auto error_code = GetOrderErrorCodeFromFtx(json["error"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetFtxExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[FtxCancelOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  // resp->set_cancel_confirmed(true);  // linchuan: removed for fake cancel confirm
}

void FtxFuturesParser::ParseRestDeposit() {
  /*
  {
    "success": true,
    "result": [
      {
        "coin": "TUSD",
        "confirmations": 64,
        "confirmedTime": "2019-03-05T09:56:55.728933+00:00",
        "fee": 0,
        "id": 1,
        "sentTime": "2019-03-05T09:56:55.735929+00:00",
        "size": 99.0,
        "status": "confirmed",
        "time": "2019-03-05T09:56:55.728933+00:00",
        "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1"
      },
      {
        "id": 41224587,
        "coin": "USD",
        "size": 100000,
        "time": "2022-09-08T07:55:26.959247+00:00",
        "notes": "Transfer from xunke23 to main account",
        "status": "complete"
      },
      {
        "id": 41543857,
        "coin": "SRM_LOCKED",
        "size": 220.32,
        "time": "2022-09-12T23:47:02.547108+00:00",
        "notes": "SRM Airdrop for FTT holdings",     # The current setting is TransferRequestProto::DEPOSIT
        "status": "complete"
      }
    ]
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestDeposit] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsFtxErrMessage(document)) {
    LOG(ERROR) << "[ParseRestDeposit] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("result") || !document["result"].IsArray()) {
    LOG(ERROR) << "[ParseRestDeposit] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document["result"].GetArray()) {
    static std::vector<std::string> fields = {"coin", "size", "status", "time", "id"};
    if (!JsonFieldCheck(data, fields)) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid transfer format" << JsonToString(data);
      continue;
    }

    if (!symcache_->has_currency_native(data["coin"].GetString())) {
      continue;
    }

    std::string transfer_id = data["id"].GetString();
    if (transfer_deposit_map_.count(transfer_id) <= 0) {
      transfer_deposit_map_[transfer_id] = "";
    }
    std::string status = data["status"].GetString();
    if ((status != "cancelled") && (status != "unconfirmed") &&
        (status != "confirmed") && (status != "requested") &&
        (status != "processing") && (status != "complete")) {
      continue;
    }
    if (status == transfer_deposit_map_[transfer_id]) {
      continue;
    }
    transfer_deposit_map_[transfer_id] = status;

    auto* currency = symcache_->GetCurrencyByNative(data["coin"].GetString());
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    currency_transfer->set_type(TransferRequestProto::DEPOSIT);
    if (data.HasMember("notes")){
      std::string notes = data["notes"].GetString();
      if (notes.find("to main account") != std::string::npos){
        currency_transfer->set_type(TransferRequestProto::WITHDRAW_SUBACCOUNT);
      }
    }

    if (status == "cancelled") {
      currency_transfer->set_status(TransferRequestProto::CANCEL_CONFIRMED);
    } else if ((status == "unconfirmed") || (status == "requested") ||
        (status == "processing")) {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_SUBMITTED);
    } else if ((status == "confirmed") || (status == "complete")) {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    }
    currency_transfer->set_transfer_id(transfer_id);
    currency_transfer->set_qty(stod(data["size"].GetString()));
    currency_transfer->set_create_timestamp(TimestampFromFtxIso8601(data["time"].GetString()));
    currency_transfer_bundle_found = true;
  }

  if (!is_transfer_deposit_initialized_) {
    is_transfer_deposit_initialized_ = true;
    LOG(INFO) << "[ParseRestDeposit] Deposit ignored: " <<
        transfer_deposit_map_.size();
    return;
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestDeposit] Deposit parsed: " <<
        currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }
}

void FtxFuturesParser::ParseRestWithdrawal() {
/*
  {
    "success": true,
    "result": [
      {
        "coin": "TUSD",
        "address": "0x83a127952d266A6eA306c40Ac62A4a70668FE3BE",
        "tag": null,
        "fee": 0,
        "id": 1,
        "size": 99.0,
        "status": "complete",
        "time": "2019-03-05T09:56:55.728933+00:00",
        "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1"
      },
      {
        "id": 33919450,
        "coin": "BTC",
        "size": 7,
        "time": "2022-05-11T21:02:21.760933+00:00",
        "notes": "Transfer from main account to xunke31",
        "status": "complete",
        "proposedTransferId": null,
        "destinationEmail": ""
      }
    ]
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestWithdrawal] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsFtxErrMessage(document)) {
    LOG(ERROR) << "[ParseRestWithdrawal] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("result") || !document["result"].IsArray()) {
    LOG(ERROR) << "[ParseRestWithdrawal] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document["result"].GetArray()) {
    static std::vector<std::string> fields = {"coin", "size", "status", "time", "id"};
    if (!JsonFieldCheck(data, fields)) {
      LOG(ERROR) << "[ParseRestWithdrawal] Invalid transfer format" << JsonToString(data);
      continue;
    }

    if (!symcache_->has_currency_native(data["coin"].GetString())) {
      continue;
    }

    std::string transfer_id = data["id"].GetString();
    if (transfer_withdrawal_map_.count(transfer_id) <= 0) {
      transfer_withdrawal_map_[transfer_id] = "";
    }
    std::string status = data["status"].GetString();
    if ((status != "cancelled") && (status != "unconfirmed") &&
        (status != "confirmed") && (status != "requested") &&
        (status != "processing") && (status != "complete")) {
      continue;
    }
    if (status == transfer_withdrawal_map_[transfer_id]) {
      continue;
    }
    transfer_withdrawal_map_[transfer_id] = status;

    auto* currency = symcache_->GetCurrencyByNative(data["coin"].GetString());
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    currency_transfer->set_type(TransferRequestProto::WITHDRAW);
    if (data.HasMember("notes") && !data.HasMember("address")){
      std::string notes = data["notes"].GetString();

      if (notes.find("Transfer from main account to ") != std::string::npos){
        currency_transfer->set_type(TransferRequestProto::DEPOSIT_SUBACCOUNT);
      }
    }

    if (status == "cancelled") {
      currency_transfer->set_status(TransferRequestProto::CANCEL_CONFIRMED);
    } else if ((status == "unconfirmed") || (status == "requested") ||
        (status == "processing")) {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_SUBMITTED);
    } else if ((status == "confirmed") || (status == "complete")) {
      currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    }

    currency_transfer->set_transfer_id(transfer_id);
    currency_transfer->set_qty(stod(data["size"].GetString()));
    currency_transfer->set_create_timestamp(TimestampFromFtxIso8601(data["time"].GetString()));
    currency_transfer_bundle_found = true;
  }

  if (!is_transfer_withdrawal_initialized_) {
    is_transfer_withdrawal_initialized_ = true;
    LOG(INFO) << "[ParseRestWithdrawal] Withdrawal ignored: " <<
        transfer_withdrawal_map_.size();
    return;
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestWithdrawal] Withdrawal parsed: " <<
        currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }
}

void FtxFuturesParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsOrderUpdate(document)) {
    std::string channel = document["channel"].GetString();
    if (channel == "fills") {
      *result_.mutable_fill_list() = ParseWsFillInfoListV2(document);
      result_.set_type(MsgType::ORDER_FILL);
    } else {
      *result_.mutable_order_list() = ParseWsOrderInfoListV2(document);
      result_.set_type(MsgType::ORDER_STATUS);
    }
  }
}

void FtxFuturesParser::ParseFixMessage() {
  if (!msg_ || msg_->fix_message == nullptr) {
    return;
  }
  const FIX::Message& message = *(msg_->fix_message);

  DLOG(INFO) << message;

  FIX::MsgType msg_type;
  if (!message.getHeader().getFieldIfSet(msg_type)) {
    return;
  }
  if (MSG_TYPE_SESSION_REJECT == msg_type) {
    // TODO(daniel) - session reject
  } else if (MSG_TYPE_EXECUTION_REPORT == msg_type) {
    ParseFixSubmitResponse(message);
    ParseFixCancelResponse(message);
    ParseFixFillInfo(message);
    ParseFixOrderInfo(message);
  } else if (MSG_TYPE_CANCEL_REJECT == msg_type) {
    ParseFixCancelResponse(message);
  }
}

void FtxFuturesParser::ParseFixSubmitResponse(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_ORDER_REJECTED != exec_type && EXEC_TYPE_ORDER_ACCEPTTED != exec_type) {
    return;
  }
  std::optional<int64_t> proc_order_id;
  FIX::ClOrdID client_id;
  if (message.getFieldIfSet(client_id)) {
    proc_order_id = ToInt64(client_id.getValue());
  }
  if (!proc_order_id) {
    LOG(ERROR) << "proc_order_id not found. " << message;
    return;
  }
  FIX::Symbol symbol;
  message.getFieldIfSet(symbol);

  SubmitOrderResponse& resp = *result_.mutable_submit_response();
  resp.Clear();
  resp.set_symbol(symbol.getValue());
  resp.set_proc_order_id(*proc_order_id);
  resp.set_timestamp(msg_->timestamp);
  if (EXEC_TYPE_ORDER_REJECTED == exec_type) {
    FIX::OrdRejReason error_code;
    resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    if (message.getFieldIfSet(error_code)) {
      if (99 == error_code) {
        resp.set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
      }
    }
    FIX::Text error_reason;
    if (message.getFieldIfSet(error_reason)) {
      resp.set_exchange_error_msg(error_reason.getValue());
    }
    resp.set_success(false);
  } else if (EXEC_TYPE_ORDER_ACCEPTTED == exec_type) {
    FIX::OrderID order_id;
    message.getFieldIfSet(order_id);
    resp.set_exchange_order_id(order_id.getValue());
    resp.set_success(true);
  } else {
    // shouldn't go here
  }
  result_.set_type(MsgType::SUBMIT_RESPONSE);
}

void FtxFuturesParser::ParseFixCancelResponse(const FIX::Message& message) {
  FIX::MsgType msg_type;
  if (!message.getHeader().getFieldIfSet(msg_type)) {
    return;
  }
  if (MSG_TYPE_EXECUTION_REPORT != msg_type && MSG_TYPE_CANCEL_REJECT != msg_type) {
    return;
  }
  if (MSG_TYPE_EXECUTION_REPORT == msg_type) {
    FIX::ExecType exec_type;
    if (!message.getFieldIfSet(exec_type)) {
      return;
    }
    if (EXEC_TYPE_CANCEL_ACCEPTTED != exec_type) {
      return;
    }
    std::optional<int64_t> proc_order_id;
    FIX::ClOrdID client_id;
    if (message.getFieldIfSet(client_id)) {
      proc_order_id = ToInt64(client_id.getValue());
    }
    if (!proc_order_id) {
      LOG(ERROR) << "proc_order_id not found. " << message;
      return;
    }
    FIX::OrderID order_id;
    message.getFieldIfSet(order_id);

    CancelOrderResponse& resp = *result_.mutable_cancel_response();
    resp.Clear();
    resp.set_proc_order_id(*proc_order_id);
    resp.set_exchange_order_id(order_id.getValue());
    resp.set_timestamp(msg_->timestamp);
    resp.set_success(true);
    result_.set_type(MsgType::CANCEL_RESPONSE);
  } else if (MSG_TYPE_CANCEL_REJECT == msg_type) {
    std::optional<int64_t> proc_order_id;
    FIX::OrigClOrdID client_id;
    if (message.getFieldIfSet(client_id)) {
      proc_order_id = ToInt64(client_id.getValue());
    }
    if (!proc_order_id) {
      LOG(ERROR) << "proc_order_id not found. " << message;
      return;
    }
    FIX::OrderID order_id;
    message.getFieldIfSet(order_id);

    CancelOrderResponse& resp = *result_.mutable_cancel_response();
    resp.Clear();
    resp.set_proc_order_id(*proc_order_id);
    resp.set_exchange_order_id(order_id.getValue());
    resp.set_timestamp(msg_->timestamp);
    resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    FIX::CxlRejReason error_code;
    if (message.getFieldIfSet(error_code)) {
      if (0 == error_code) {
        resp.set_error_code(coin::proto::OrderErrorCode::ORDER_CANCELED_ALREADY);
      } else if (1 == error_code) {
        resp.set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
      }
    }
    resp.set_success(false);
    result_.set_type(MsgType::CANCEL_RESPONSE);
  } else {
    // shouldn't go here
  }
}

void FtxFuturesParser::ParseFixFillInfo(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_ORDER_FILL != exec_type) {
    return;
  }
  std::optional<int64_t> proc_order_id;
  FIX::ClOrdID client_id;
  if (message.getFieldIfSet(client_id)) {
    proc_order_id = ToInt64(client_id.getValue());
  }
  if (!proc_order_id) {
    LOG(ERROR) << "proc_order_id not found. " << message;
    return;
  }
  FIX::OrderID order_id;
  FIX::Symbol symbol;
  FIX::LastPx price;
  FIX::LastQty order_qty;
  FIX::FillTradeID fill_id;
  FIX::AggressorIndicator aggressor_ind;
  message.getFieldIfSet(order_id);
  message.getFieldIfSet(symbol);
  message.getFieldIfSet(price);
  message.getFieldIfSet(order_qty);
  message.getFieldIfSet(fill_id);
  message.getFieldIfSet(aggressor_ind);

  ProductFillBundle& product_fill_bundle = *result_.mutable_fill_list();
  product_fill_bundle.Clear();
  auto fill = product_fill_bundle.add_each_fill();
  fill->Clear();
  fill->set_symbol(symbol.getValue());
  fill->set_fill_timestamp(msg_->timestamp);
  fill->set_exchange_order_id(order_id.getValue());
  fill->set_proc_order_id(*proc_order_id);
  fill->set_price(price.getValue());
  fill->set_qty(order_qty.getValue());
  fill->set_fill_id(fill_id.getValue());
  if (aggressor_ind.getValue()) {
    fill->set_fill_type(FillType::TAKER_FILL_TYPE);
  } else {
    fill->set_fill_type(FillType::MAKER_FILL_TYPE);
  }
  result_.set_type(MsgType::ORDER_FILL);
}

void FtxFuturesParser::ParseFixOrderInfo(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }

  if (EXEC_TYPE_NEW_ORDER == exec_type || EXEC_TYPE_ORDER_FILL == exec_type ||
      EXEC_TYPE_ORDER_DONE == exec_type || EXEC_TYPE_ORDER_CANCELLED == exec_type) {
    FIX::OrderID order_id;
    FIX::Symbol symbol;
    FIX::Price price;
    FIX::LeavesQty order_qty;
    FIX::ClOrdID client_id;
    FIX::Side side;
    FIX::OrdStatus order_status;
    message.getFieldIfSet(order_id);
    message.getFieldIfSet(symbol);
    message.getFieldIfSet(price);
    message.getFieldIfSet(order_qty);
    message.getFieldIfSet(client_id);
    message.getFieldIfSet(side);
    message.getFieldIfSet(order_status);
    auto proc_order_id = ToInt64(client_id.getValue());
    if (!proc_order_id) {
      return;
    }

    ProductOrderBundle& product_order_bundle = *result_.mutable_order_list();
    product_order_bundle.Clear();
    auto order = product_order_bundle.add_each_order();
    order->Clear();
    order->set_market_type(MarketType::Futures);
    order->set_exchange_type(ExchangeType::Ftx);
    order->set_api_version("v1");
    order->set_exchange_order_id(order_id.getValue());
    order->set_symbol(symbol.getValue());
    order->set_price(price.getValue());
    order->set_qty(order_qty.getValue());
    order->set_proc_order_id(*proc_order_id);
    if ('1' == side.getValue()) {
      order->set_side(coin::proto::OrderSide::BUY_ORDER);
    } else if ('2' == side.getValue()) {
      order->set_side(coin::proto::OrderSide::SELL_ORDER);
    }
    order->set_timestamp(msg_->timestamp);
    // TODO(daniel) - confirm the pending cancel order
    switch (order_status) {
      case ORD_STATUS_NEW_ORDER:
      case ORD_STATUS_PARTIALLY_FILLED:
      case ORD_STATUS_RESIZED_ORDER:
      case ORD_STATUS_PENDING_CANCEL:
        order->set_is_live(true);
        break;
      case ORD_STATUS_FULLY_FILLED:
        order->set_fully_filled(true);
        break;
      case ORD_STATUS_CANCELLED_ORDER:
        order->set_cancel_confirmed(true);
        break;
      default:
        LOG(ERROR) << "[ParseFIXOrderInfo] Unknown order status! " << order_status;
        break;
    }
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

auto FtxFuturesParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  if (msg.type == OrderSubTopicId::QUICKFIX_MESSAGE) {
    ParseFixMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      CHECK(false);
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestOrdersInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::REST_ACCOUNT_POSITION:
      ParseRestAccountPosition();
      break;
    case FtxOrderSubTopicId::REST_DEPOSIT:
      ParseRestDeposit();
      break;
    case FtxOrderSubTopicId::REST_WITHDRAWAL:
      ParseRestWithdrawal();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::ftx_futures::order_v1
